package com.bravo.other.geoid;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

/**
 * Geoid Height Calculator
 * Geoid Height = Geoid Elevation - Ellipsoid Elevation
 * 
 * @author bravo
 */
public class GeoidHeightCalculator {
    
    /**
     * Calculate geoid height using EGM2008 model
     * EGM2008 is a global geoid model with high accuracy
     * 
     * @param latitude latitude in degrees (-90 to 90)
     * @param longitude longitude in degrees (-180 to 180)
     * @return geoid height in meters
     */
    public static double calculateEGM2008(double latitude, double longitude) {
        // Simplified EGM2008 model calculation
        // In practice, full spherical harmonic expansion is needed
        
        // Convert degrees to radians
        double latRad = Math.toRadians(latitude);
        double lonRad = Math.toRadians(longitude);
        
        // Simplified spherical harmonic coefficients (EGM2008 has more coefficients)
        double[] coefficients = {
            0.0,  // 0th degree 0th order
            -0.484165371736E-03,  // 1st degree 0th order
            -0.206615509074E-09,  // 2nd degree 0th order
            -0.138730634851E-09,  // 2nd degree 1st order
            -0.157400953592E-09   // 2nd degree 2nd order
        };
        
        // Calculate spherical harmonic values
        double geoidHeight = 0.0;
        
        // 0th degree term
        geoidHeight += coefficients[0];
        
        // 1st degree term
        geoidHeight += coefficients[1] * Math.cos(latRad);
        
        // 2nd degree terms
        geoidHeight += coefficients[2] * (3.0 * Math.pow(Math.sin(latRad), 2) - 1.0) / 2.0;
        geoidHeight += coefficients[3] * 3.0 * Math.sin(latRad) * Math.cos(latRad) * Math.cos(lonRad);
        geoidHeight += coefficients[4] * 3.0 * Math.pow(Math.cos(latRad), 2) * Math.cos(2.0 * lonRad);
        
        return geoidHeight;
    }
    
    /**
     * Calculate geoid height using EGM96 model
     * EGM96 is an earlier global geoid model
     * 
     * @param latitude latitude in degrees
     * @param longitude longitude in degrees
     * @return geoid height in meters
     */
    public static double calculateEGM96(double latitude, double longitude) {
        // Simplified EGM96 model
        double latRad = Math.toRadians(latitude);
        double lonRad = Math.toRadians(longitude);
        
        // Main EGM96 coefficients
        double C20 = -0.484165371736E-03;
        double C22 = 0.243914352398E-06;
        double S22 = -0.140016683654E-05;
        
        // Calculate geoid height
        double geoidHeight = 0.0;
        
        // Main term: C20 term (flattening term)
        geoidHeight += C20 * (3.0 * Math.pow(Math.sin(latRad), 2) - 1.0) / 2.0;
        
        // C22 and S22 terms (ellipticity terms)
        geoidHeight += C22 * 3.0 * Math.pow(Math.cos(latRad), 2) * Math.cos(2.0 * lonRad);
        geoidHeight += S22 * 3.0 * Math.pow(Math.cos(latRad), 2) * Math.sin(2.0 * lonRad);
        
        return geoidHeight;
    }
    
    /**
     * Calculate geoid height using online service
     * Calls NOAA's online geoid height calculation service
     * 
     * @param latitude latitude in degrees
     * @param longitude longitude in degrees
     * @return geoid height in meters
     * @throws IOException network request exception
     */
    public static double calculateOnline(double latitude, double longitude) throws IOException {
        String urlString = String.format(
            "https://www.ngs.noaa.gov/cgi-bin/geoid_lookup.prl?lat=%.6f&lon=%.6f&units=meters",
            latitude, longitude
        );
        
        URL url = new URL(urlString);
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setRequestMethod("GET");
        
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(connection.getInputStream()))) {
            
            String line;
            while ((line = reader.readLine()) != null) {
                if (line.contains("Geoid Height:")) {
                    // Parse the returned HTML page to extract geoid height value
                    String[] parts = line.split("Geoid Height:");
                    if (parts.length > 1) {
                        String valueStr = parts[1].trim();
                        // Extract numeric part
                        valueStr = valueStr.replaceAll("[^0-9.-]", "");
                        return Double.parseDouble(valueStr);
                    }
                }
            }
        }
        
        throw new IOException("Unable to get geoid height data from online service");
    }
    
    /**
     * Calculate geoid height using interpolation method
     * Based on known control points for bilinear interpolation
     * 
     * @param latitude latitude in degrees
     * @param longitude longitude in degrees
     * @param controlPoints control point data
     * @return geoid height in meters
     */
    public static double calculateByInterpolation(double latitude, double longitude, 
                                                Map<Coordinate, Double> controlPoints) {
        // Find the nearest four control points
        Coordinate[] nearestPoints = findNearestPoints(latitude, longitude, controlPoints);
        
        if (nearestPoints.length < 4) {
            throw new IllegalArgumentException("Insufficient control points for interpolation");
        }
        
        // Bilinear interpolation
        double lat1 = nearestPoints[0].latitude;
        double lat2 = nearestPoints[2].latitude;
        double lon1 = nearestPoints[0].longitude;
        double lon2 = nearestPoints[1].longitude;
        
        double h11 = controlPoints.get(nearestPoints[0]); // bottom-left
        double h12 = controlPoints.get(nearestPoints[1]); // bottom-right
        double h21 = controlPoints.get(nearestPoints[2]); // top-left
        double h22 = controlPoints.get(nearestPoints[3]); // top-right
        
        // Bilinear interpolation formula
        double t = (latitude - lat1) / (lat2 - lat1);
        double u = (longitude - lon1) / (lon2 - lon1);
        
        double geoidHeight = (1 - t) * (1 - u) * h11 +
                           (1 - t) * u * h12 +
                           t * (1 - u) * h21 +
                           t * u * h22;
        
        return geoidHeight;
    }
    
    /**
     * Find nearest control points
     */
    private static Coordinate[] findNearestPoints(double latitude, double longitude, 
                                                Map<Coordinate, Double> controlPoints) {
        // Simplified nearest point finding algorithm
        // In practice, more efficient algorithms like KD-tree can be used
        
        Coordinate[] nearest = new Coordinate[4];
        double minDistance = Double.MAX_VALUE;
        
        for (Coordinate coord : controlPoints.keySet()) {
            double distance = Math.sqrt(
                Math.pow(coord.latitude - latitude, 2) + 
                Math.pow(coord.longitude - longitude, 2)
            );
            
            if (distance < minDistance) {
                minDistance = distance;
                // Simplified processing here, should find 4 nearest points
            }
        }
        
        // Return found points (simplified processing)
        return controlPoints.keySet().toArray(new Coordinate[0]);
    }
    
    /**
     * Coordinate point class
     */
    public static class Coordinate {
        public final double latitude;
        public final double longitude;
        
        public Coordinate(double latitude, double longitude) {
            this.latitude = latitude;
            this.longitude = longitude;
        }
        
        @Override
        public boolean equals(Object obj) {
            if (this == obj) return true;
            if (obj == null || getClass() != obj.getClass()) return false;
            Coordinate that = (Coordinate) obj;
            return Double.compare(that.latitude, latitude) == 0 &&
                   Double.compare(that.longitude, longitude) == 0;
        }
        
        @Override
        public int hashCode() {
            return Double.hashCode(latitude) * 31 + Double.hashCode(longitude);
        }
    }
    
    /**
     * Create sample control point data
     */
    public static Map<Coordinate, Double> createSampleControlPoints() {
        Map<Coordinate, Double> controlPoints = new HashMap<>();
        
        // Add some sample control points (latitude, longitude, geoid height)
        controlPoints.put(new Coordinate(39.9042, 116.4074), -0.5);  // Beijing
        controlPoints.put(new Coordinate(31.2304, 121.4737), -0.3);  // Shanghai
        controlPoints.put(new Coordinate(23.1291, 113.2644), -0.2);  // Guangzhou
        controlPoints.put(new Coordinate(30.2741, 120.1551), -0.4);  // Hangzhou
        controlPoints.put(new Coordinate(22.3193, 114.1694), -0.1);  // Shenzhen
        controlPoints.put(new Coordinate(29.8683, 121.5440), -0.3);  // Ningbo
        controlPoints.put(new Coordinate(32.0603, 118.7969), -0.4);  // Nanjing
        controlPoints.put(new Coordinate(36.6512, 117.1201), -0.6);  // Jinan
        
        return controlPoints;
    }
}
